import {Cask} from 'cascadium';

import {Rule, conform} from "cascadium-formulae";

import Category from '../src/Category';
import Voucher from '../src/Voucher';
import Accrual from '../src/Accrual';

export const castMethods = {
  '!': () => false,
  '*': () => true,
  '=': (propVal, pattern) => {
    return propVal.replace(/[\s.]+/g, '') === pattern
  },
  '~/': (propVal, pattern) => {
    return conform(propVal.replace(/[\s.]+/g, ''), pattern)
  },
  '~~/': (propVal, pattern) => {
    return conform(propVal.replace(/[\s.]+/g, ''), pattern, {exactMatching: false})
  },
};

const markAssign = (rec, mark, section='CASHFLOW') => {
  rec.attr.assigned ??= {};
  rec.attr.assigned[section] = mark;
}

export const formCashflowTable = (cashflowTableEntry) => {
  return new Category({code: '', name: '*'})
  .nestFrom(cashflowTableEntry.map(({code, name}) => {
    return new Category({code, name});
  }));
}


export const targetMethods = {
  CONS: (target, source) => source.move(target, {withPath: false, withProto: false}),
  MOVE: (target, source) => {
    markAssign(source, 'ASSIGNED');
    source.move(target, {withPath: false, withProto: false})
  },
  MOVE_ADDI: (target, source) => {
    markAssign(source, 'ASSIGNED', 'ADDITIONAL');
    source.move(target, {withPath: false, withProto: false})
  },
  NULL: (target, source) => {}
};

const createSub = (voucher, dest) => {

  const {accrual: {cr, dr}} = voucher;

  voucher.attr.level = 'DEST_CATE';
  voucher.desc = 'DEDUCTED MONETARY ACCRUAL';
  voucher.dest = dest;

  const subCr = new Voucher({...voucher, accrual: new Accrual({cr}), attr:{level: 'DIRECTION', dir: 'cr'}});
  const subDr = new Voucher({...voucher, accrual: new Accrual({dr}), attr:{level: 'DIRECTION', dir: 'dr'}});

  subCr.move(voucher);
  subDr.move(voucher);

}

export const expandMethods = {
  SELF: rec => [rec],
  NEXT: rec => rec.succ,
  REST: rec => rec.succ.unnest(({succ}) => succ),
  LAST: rec => rec.succ.unnest(({succ}) => succ).all('succ.length', 0),
  NULL: rec => {
    markAssign(rec, 'VISITED');
    return [rec];
  },
  DIRECTION: rec => {
    markAssign(rec, 'VISITED');
    return rec.unnestTo().all('attr.level', 'DIRECTION');
  },
  DEST_CATE: rec => {
    markAssign(rec, 'VISITED');
    return rec.unnestTo().all('attr.level', 'DEST_CATE');
  },
  DEST_CATE_VIRTUAL_MONETARY: rec => {
    markAssign(rec, 'VISITED');

    const destCates = rec.unnestTo().all('attr.level', 'DEST_CATE');
    const destCateMap = destCates.grip('curr.name').getDict();

    const newDestCates = new Cask();

    for (let [name, dests] of Object.entries(destCateMap)) {

      const monetary = [];
      const inventory = [];
  
      for (let i = 0; i < dests.length; i++) {
        const {dest} = dests[i];
        if (['现金', '库存现金', '银行存款', '其他货币资金'].includes(dest.ref.path.split("/")[0])) {
          monetary.push(dests[i]);
        } else {
          inventory.push(dests[i]);
        }
      }
  
      const virtualizedTotal = Voucher.from(monetary);

      const {decomp, rest} = virtualizedTotal.decomposeAgainst(inventory);
  
      for (let i = 0; i < decomp.length; i++) {
        createSub(decomp[i], inventory[i].dest);
      }

      newDestCates.push(...inventory);
      newDestCates.push(...monetary);
  
      // 如果预收/预付的明细科目中包含有对货币资金的发生，但是不存在对其他科目的发生，
      // 则认为是购买了固定资产。需要注意的是，这里的path不再指向一个末级科目，原因是
      // 这个path仅用于分配至现流表的规则判断，且不会在总账中体现出来。

      const {accrual:{cr, dr}} = rest;
      if (!(cr === 0 && dr === 0)) {
        createSub(rest, {ref: {path: '固定资产'}});
        newDestCates.push(rest);
      }
    }

    return newDestCates;
  }
}

const overridedSubRule = (patt, succ) => {

  const theRule = new Rule({
    prop: 'dest.ref.path',
    cast: '~/',
    overProp:'dest.ref.path',
    overCast: '~~/',
    patt,
    action: 'SELF',
  });

  theRule.succ = succ;

  return theRule
}

const addOverridedRule = (name, action, topmost) => {

  const destCateRule = new Rule({patt: name, prop:'path', cast:'~/', action});

  overridedSubRule('**', topmost.succ).moveTo(destCateRule),
  destCateRule.moveTo(topmost);
}

export const formCashflowRulesMain = (cashflowTable, cashflowRulesEntry) => {

  const cashflowEndMap = cashflowTable.succ
    .unnest(({succ}) => succ)
    .grip('curr.name')
    .map(([k, [v]]) => [k, v])
    .getDict();

  const topmost = new Rule({cast: '*'});


  // 按照被分配科目以及借贷方建立规则，首先建立按借贷方发生额的规则
  // 因为这是直接与现金流量表关联的规则，因此首先被建立起来，并且将
  // 现金流量表的条目写入对应的dest属性中去。

  // 分配方法为MOVE，即直接将借方或贷方的汇总分录移入现流表末级条目
  // 之下。
    
  for (let spec of cashflowRulesEntry) {

    let {name, method} = spec;

    if (method?.startsWith('!')) {

      new Rule({patt: name, prop:'path', action: 'NULL'}).moveTo(topmost);

    } else if (method?.startsWith('@')) {

      addOverridedRule(name, 'DEST_CATE_VIRTUAL_MONETARY', topmost);

    } else if (method?.startsWith('?')) {

      addOverridedRule(name, 'DEST_CATE', topmost);

    } else {

      const rule = new Rule({ patt: name, prop: 'path', cast: '~/',  action: 'DIRECTION'});

      const expandSpec = (dir) => ({
        prop:'attr.dir',
        patt: dir,  
        cast:'=', 
        dest: cashflowEndMap[spec[dir]], 
        action: 'MOVE'
      })

      new Rule(expandSpec('dr')).moveTo(rule);
      new Rule(expandSpec('cr')).moveTo(rule);

      rule.moveTo(topmost)
    }
  };

  return topmost;
}

export const formCashflowRulesAdditional = (cashflowTable, cashflowRulesEntry) => {

  const cashflowEndMap = cashflowTable.succ
    .unnest(({succ}) => succ)
    .grip('curr.name')
    .map(([k, [v]]) => [k, v])
    .getDict();

  const topmost = new Rule({cast: '*'});

  for (let spec of cashflowRulesEntry) {

    let {name, method} = spec;

    if (method?.startsWith('!')) {

      new Rule({patt: name, prop:'path', action: 'NULL'}).moveTo(topmost);

    } else {

      const rule = new Rule({ patt: name, prop: 'path', cast: '~~/',  action: 'SELF'});

      const expandSpec = (dir) => ({
        prop:'attr.dir',
        patt: dir,  
        cast:'=', 
        dest: cashflowEndMap[spec[dir]], 
        action: 'MOVE_ADDI'
      })
  
      new Rule(expandSpec('dr')).moveTo(rule);
      new Rule(expandSpec('cr')).moveTo(rule);
  
      rule.moveTo(topmost)
    }
  };

  return topmost;
}

export const travAssign = (rules) => (cate) => {

  if(cate.attr.level === 'CATEGORY') {
    if (!cate.backtrav().some(({attr}) => attr.assigned?.CASHFLOW === 'VISITED')) {
      rules.match(cate, {castMethods, expandMethods, targetMethods});
    }
  }
}

export const travAssignUncate = (uncate) => (cate) => {

  const {attr:{level, assigned={}}} = cate;
  
  if (level === 'DIRECTION' && !assigned.CASHFLOW) {
    
    cate.move(uncate, {withProto: false, withPath: false});
    markAssign(cate, 'UNCATED');    

    return true;
  }

}

export const assignCashflowMain = (category, cashflowTable, cashflowRules) => {

  category.trav(travAssign(cashflowRules));
  
  const uncategorized = cashflowTable.succ.find(({curr:{code}}) => code === '0000');

  category.trav(travAssignUncate(uncategorized));

}

export const assignCashflowAdditional = (cashflowTable, cashflowRules) => {

  const operational = cashflowTable.succ
    .find(({curr:{name}}) => name === '一、经营活动产生的现金流量：')
    .unnestTo()
    .filter(({attr:{level}}) => level === 'DIRECTION');

  const uncategorized = cashflowTable.succ.find(({curr:{code}}) => code === '8000');

  for (let i = 0; i < operational.length; i++) {
    const cate = operational[i];
    cashflowRules.match(cate, {castMethods, expandMethods, targetMethods});

    if (!cate.attr.assigned.ADDITIONAL) {
      cate.move(uncategorized, {withProto: false, withPath: false});
      markAssign(cate, 'UNCATED', 'ADDITIONAL');
    }
  }
}